package bjut.core.extension;

import bjut.core.bean.Holder;
import bjut.core.annotation.SPI;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * @Author: zhuzhiming
 * @Date: 2022/4/10 13:32
 */
@Slf4j
public final class ExtensionLoader<T> {

    private static final String LOADER_DIRECTORY = "META-INF/extensions/";
    private static final ConcurrentHashMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADER_MAP = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Holder<Object>> holderMap = new ConcurrentHashMap<>();
    private static final ConcurrentHashMap<Class<?>, Object> EXTENSION_INSTANCE_MAP = new ConcurrentHashMap<>();
    private final Holder<Map<String, Class<?>>> holderCache = new Holder<>();
    private final Class<?> cls;
    // \s 代表空白符
    private static final Pattern DEFAULT_VALUE_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");
    private String cacheDefaultValue;

    private ExtensionLoader(Class<?> cls) {
        this.cls = cls;
    }

    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> cls) {

        if (cls == null) {
            throw new IllegalArgumentException("Extension type must not be null");
        }
        if (!cls.isInterface()) {
            throw new IllegalArgumentException("Extension type must be interface");
        }
        //cls.getAnnotation(SPI.class) == null
        if (!cls.isAnnotationPresent(SPI.class)) {
            throw new IllegalArgumentException("Extension type must has SPI annotation");
        }

        ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) EXTENSION_LOADER_MAP.get(cls);
        if (extensionLoader == null) {
            EXTENSION_LOADER_MAP.putIfAbsent(cls, new ExtensionLoader<>(cls));
            extensionLoader = (ExtensionLoader<T>) EXTENSION_LOADER_MAP.get(cls);
        }
        return extensionLoader;
    }

    public T getExtension(String name) {

        if (!StringUtils.hasLength(name)) {
            throw new IllegalArgumentException("Extension name must not be empty or null");
        }

        if ("true".equals(name)) {
            return getDefaultExtension();
        }

        Holder<Object> holder = holderMap.get(name);

        if (holder == null) {
            holderMap.putIfAbsent(name, new Holder<>());
            holder = holderMap.get(name);
        }

        Object instance = holder.getValue();
        if (instance == null) {
            synchronized (ExtensionLoader.class) {
                instance = holder.getValue();
                if (instance == null) {
                    instance = createExtension(name);
                    holder.setValue(instance);
                }
            }
        }
        return (T) instance;
    }

    private T getDefaultExtension() {
        getExtensionClass();
        if (null == cacheDefaultValue || cacheDefaultValue.length() == 0 || "true".equals(cacheDefaultValue)) {
            return null;
        }
        return getExtension(cacheDefaultValue);
    }

    private T createExtension(String name) {

        Class<?> aClass = getExtensionClass().get(name);

        if (aClass == null) {
            throw new RuntimeException("no such extension of " + name);
        }

        T instance = (T) EXTENSION_INSTANCE_MAP.get(aClass);
        if (instance == null) {
            try {
                EXTENSION_INSTANCE_MAP.putIfAbsent(aClass, aClass.newInstance());
                instance = (T) EXTENSION_INSTANCE_MAP.get(aClass);
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return instance;
    }

    private Map<String, Class<?>> getExtensionClass() {

        final SPI defaultAnnotation = cls.getAnnotation(SPI.class);
        if (defaultAnnotation != null) {
            String defaultValue = defaultAnnotation.value();
            if (defaultValue != null) {
                String[] values = DEFAULT_VALUE_SEPARATOR.split(defaultValue.trim());
                if (values.length > 1) {
                    throw new IllegalArgumentException("SPI default value size more than 1");
                }
                if (values.length == 1) {
                    cacheDefaultValue = values[0];
                }
            }
        }

        Map<String, Class<?>> classMap = holderCache.getValue();
        if (classMap == null) {
            synchronized (this) {
                classMap = holderCache.getValue();
                if (classMap == null) {
                    classMap = new HashMap<>();
                    loadDirectory(classMap);
                    holderCache.setValue(classMap);
                }
            }
        }
        return classMap;
    }

    private void loadDirectory(Map<String, Class<?>> classMap) {

        String fileName = LOADER_DIRECTORY + cls.getName();
        try {
            ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            Enumeration<URL> urls = classLoader.getResources(fileName);
            if (urls != null) {
                while (urls.hasMoreElements()) {
                    URL url = urls.nextElement();
                    loadResource(classMap, classLoader, url);
                }
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        }

    }

    private void loadResource(Map<String, Class<?>> classMap, ClassLoader classLoader, URL url) {

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                //exclude comment
                final int commentIndex = line.indexOf("#");
                if (commentIndex >= 0) {
                    line = line.substring(0, commentIndex);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        final int splitIndex = line.indexOf("=");
                        String key = line.substring(0, splitIndex).trim();
                        String value = line.substring(splitIndex + 1).trim();
                        if (key.length() > 0 && value.length() > 0) {
                            Class<?> aClass = classLoader.loadClass(value);
                            classMap.put(key, aClass);
                        }
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}
